Buying training
Who bought training in 2017? We need to grab the data about who bought and also include the students email addresses to account for students being the requesters but not the purchasers. We will need to exclude the free training options.
con %>%
tbl("woocommerce_order_items") ->
items
con %>%
tbl("posts") ->
posts
posts %>%
inner_join(items, by=c("id"="order_id")) %>%
filter(order_item_name!='Free Membership') %>%
select(order_id=id, purchase_date=post_date_gmt, order_item_name, order_item_id) %>%
collect() ->
orders
library(readxl)
"../data/Order Emails.xlsx" %>%
read_excel() %>%
select( order_id=post_id, starts_with("email"))->
orders_purchasers
con %>%
tbl("woocommerce_order_items_meta") %>%
left_join(items) %>%
select( order_id, starts_with("email")) %>%
collect() ->
orders_students
Joining, by = "order_item_id"
orders_purchasers %>%
union(orders_students) %>%
inner_join(orders)->
orders_emails
Joining, by = "order_id"
orders_emails %>%
group_by(email_user_id, email_domain_id) %>%
summarise(orders=n(),
earliest=min(purchase_date),
latest=max(purchase_date),
products=list(order_item_name)) ->
customers
Let’s take a quick look at the distribution of orders by email address.
customers %>%
arrange(desc(orders))
customers %>%
ggplot(aes(x=orders)) +
geom_histogram(bins=20)

With this list of unique email addresses involved in sales we can see how these match up against the membership list. Let’s first look at the full list to see how many people were on the list and when they joined.
con %>%
tbl("mailchimp_members") ->
mailchimp_members
customers %>%
select(-products) %>%
full_join( mailchimp_members, copy=TRUE,
by= c("email_user_id","email_domain_id")) ->
mailchimp_customers
(mailchimp_customers %>%
mutate(purchases=!is.na(orders),
mailchimp=!is.na(member_rating),
signed_after_order=confirm_time>earliest) %>%
group_by(mailchimp,purchases,signed_after_order) %>%
summarise(n=n()) ->
mailchimp_training_overlap)
Looking at the records, 24.9% of customers do not have a mailchimp subscription (currently). 16.1% appear to have become mailchimp members after purchasing. This leaves 59% of customers being on the mailing list before making their first purchase.
mailchimp_customers %>%
filter(!is.na(member_rating)) %>%
mutate(any_purchase=!is.na(orders),
purchase=!is.na(orders)&confirm_time>earliest) %>%
mutate(want_to_gets=str_split(i_want_to_get,", ")) %>%
unnest(want_to_gets) %>%
mutate(want_to_gets=coalesce(want_to_gets,"Unknown recd"), n=TRUE) %>%
spread(want_to_gets, n,fill = FALSE) %>%
mutate(signup_sources=str_split(signup_source,", ")) %>%
unnest(signup_sources) %>%
mutate(signup_sources=coalesce(signup_sources,"Unknown source"), n=TRUE) %>%
spread(signup_sources, n,fill = FALSE) ->
mailchimp_labelled
A super basic model
Without taking into behaviour in the run up of purchasing, let’s do a very quick and dirty model to see whether some of the mailchimp member level features are useful as-is for predicting purchases. We’ll use a decision tree based model for this.
library(FFTrees)
O
/ \
F O
/ \
F T
FFTrees v1.3.5. Email: Nathaniel.D.Phillips.is@gmail.com
FFTrees.guide() opens the package guide. Citation info at citation('FFTrees')
mailchimp_labelled %>%
select(-(email_user_id:client_type),-(optin_time:longitude),
-time_zone,-(region:notes), -purchase) %>%
FFTrees(any_purchase ~ ., data=.,
do.comp = FALSE,
train.p = 0.7,
sens.w = 0.75,
decision.labels = c("Didn't purchase","Purchased"),
progress = FALSE) ->
initial_tree
plot(initial_tree)

This is a simple model that correctly predicts a good proportion of the customers that purchased.
initial_tree
FFT #1 predicts any_purchase using 4 cues: {member_rating,Unknown recd,Monday Links - our favorite SQL & tech news from the week,First Responder Kit}
[1] If member_rating > 2, predict Purchased.
[2] If Unknown recd != TRUE, predict Didn't purchase.
[3] If Monday Links - our favorite SQL & tech news from the week != FALSE, predict Didn't purchase.
[4] If First Responder Kit != FALSE, predict Didn't purchase, otherwise, predict Purchased.
train test
cases :n 69363.00 29727.00
speed :mcu 1.93 1.93
frugality :pci 0.94 0.94
accuracy :acc 0.65 0.65
weighted :wacc 0.75 0.74
sensitivity :sens 0.78 0.77
specificity :spec 0.65 0.65
pars: algorithm = 'ifan', goal = 'wacc', goal.chase = 'bacc', sens.w = 0.75, max.levels = 4
This model heavily emphasised correctly identifying the people who would purchase at the expense of including more people who wouldn’t purchase. We get a get a high degree of sensitivity (correctly predicting those who purchased) from the mailchimp engagement figure, people ticking what types of emails they want to receive and them not wanting the Monday email.
This model will be our baseline when we get into building more models.
Opens
Now that we have for each person on the mailing list, whether they made a purchase or not, we should build some features around how often people have opened the newsletters in the past.
library(padr)
library(timeDate)
mailchimp_labelled %>%
thicken("week","start_week",by="confirm_time") %>%
mutate(earliest=coalesce(earliest, as.POSIXct("2017-12-31"))) %>%
thicken("week","sale_week",by="earliest") %>%
select(email_user_id, start_week, sale_week) ->
chimp_lite
opens <- dbGetQuery(con,
"select
email_user_id,
count(*) as n
from anon.mailchimp_opens o
group by email_user_id,
CONVERT(date, o.click_timestamp)")
as.Date("2011-01-01") %>%
seq.Date(as.Date("2018-01-01"), 1) ->
dates_seq
dates_seq %>%
format() %>%
timeDate() ->
dates_timeDate
holiday<-isHoliday(dates_timeDate,holidayNYSE(2011:2018))
weekday<-isWeekday(dates_timeDate)
dates_lookup<-tibble(click_timestamp=dates_seq,
holiday,
weekday,
weekend=!weekday)
opens %>%
left_join(dates_lookup)->
opens
Joining, by = "click_timestamp"
opens %>%
thicken("week", "click") %>%
inner_join(chimp_lite, by="email_user_id") %>%
mutate(start_week_diff=difftime(click, start_week, units="week"),
sale_week_diff=difftime(sale_week, click, units="week")) %>%
mutate(start_month=(as.integer(start_week_diff) %/% 4) +1,
sale_month=(as.integer(sale_week_diff) %/% 4) +1 ) ->
opens
opens %>%
sample_n(200)
Opens by usage
(opens %>%
group_by(email_user_id) %>%
summarise(holiday_open_raw=sum(holiday*n),
holiday_open_prop=holiday_open_raw/sum(n),
weekday_open_raw=sum(weekday*n),
weekday_open_prop=weekday_open_raw/sum(n),
weekend_open_raw=sum(weekend*n),
weekend_open_prop=weekend_open_raw/sum(n)
) ->
opens_typeofday)
Opens over time
opens %>%
filter(start_month > 0) %>%
mutate(since_start_h=start_month %/% 6) %>%
group_by(email_user_id,since_start_h) %>%
summarise(opens=sum(n),active=TRUE) %>%
group_by(email_user_id) %>%
mutate(open_prop=opens/sum(opens),
since_start_h=paste0("h",since_start_h)) ->
opens_start_hl
opens_start_hl %>%
mutate(since_start_h=paste0(since_start_h,"_opens")) %>%
spread(since_start_h, opens, fill=0) ->
opens_start_opens
opens_start_hl %>%
select(email_user_id:since_start_h, active) %>%
mutate(since_start_h=paste0(since_start_h,"_opensactive")) %>%
spread(since_start_h, active, fill=FALSE) ->
opens_start_active
opens_start_hl %>%
select(email_user_id:since_start_h, open_prop) %>%
mutate(since_start_h=paste0(since_start_h,"_opensprop")) %>%
spread(since_start_h, open_prop, fill=0) ->
opens_start_prop
(opens_start_opens %>%
left_join(opens_start_active) %>%
left_join(opens_start_prop) ->
opens_activity)
rm(list=c("opens","opens_start_hl",
"opens_start_opens","opens_start_active",
"opens_start_prop"))
Clicks
clicks <- dbGetQuery(con,
"select
email_user_id,
CONVERT(date, o.click_timestamp) as click_timestamp,
o.click_url,
count(*) as n
from anon.mailchimp_clicks o
group by email_user_id,
CONVERT(date, o.click_timestamp),
o.click_url")
clicks %>%
left_join(dates_lookup)->
clicks
clicks %>%
thicken("week", "click") %>%
inner_join(chimp_lite, by="email_user_id") %>%
mutate(start_week_diff=difftime(click, start_week, units="week"),
sale_week_diff=difftime(sale_week, click, units="week")) %>%
mutate(start_month=(as.integer(start_week_diff) %/% 4) +1,
sale_month=(as.integer(sale_week_diff) %/% 4) +1 ) ->
clicks
Clicks by usage
(clicks %>%
group_by(email_user_id) %>%
summarise(holiday_clicks_raw=sum(holiday*n),
holiday_clicks_prop=holiday_clicks_raw/sum(n),
weekday_clicks_prop=weekday_clicks_raw/sum(n),
weekend_clicks_raw=sum(weekend*n),
weekend_clicks_prop=weekend_clicks_raw/sum(n)
) ->
clicks_typeofday)
Clicks over time
clicks %>%
filter(start_month > 0) %>%
mutate(since_start_h=start_month %/% 6) %>%
group_by(email_user_id,since_start_h) %>%
summarise(clicks=sum(n),active=TRUE) %>%
group_by(email_user_id) %>%
mutate(click_prop=clicks/sum(clicks),
since_start_h=paste0("h",since_start_h)) ->
clicks_start_hl
clicks_start_hl %>%
select(email_user_id:since_start_h, clicks) %>%
mutate(since_start_h=paste0(since_start_h,"_clicks")) %>%
spread(since_start_h, clicks, fill=0) ->
clicks_start_clicks
clicks_start_hl %>%
select(email_user_id:since_start_h, active) %>%
mutate(since_start_h=paste0(since_start_h,"_clickactive")) %>%
spread(since_start_h, active, fill=FALSE) ->
clicks_start_active
clicks_start_hl %>%
select(email_user_id:since_start_h, click_prop) %>%
mutate(since_start_h=paste0(since_start_h,"_clickprop")) %>%
spread(since_start_h, click_prop, fill=0) ->
clicks_start_prop
(clicks_start_clicks %>%
left_join(clicks_start_active) %>%
left_join(clicks_start_prop) ->
click_activity)
rm(list=c("clicks_start_hl",
"clicks_start_clicks","clicks_start_active",
"clicks_start_prop"))
Clicks by topic
For how we arrived at topics per url, check out What topics did people click?
clicks %>%
inner_join({
"../data/link_lookup.csv" %>%
read_csv()
}, by="click_url") %>%
left_join({
"../data/urltopics.csv"%>%
read_csv()
}, by=c("clean_file"="url")) %>%
mutate(brent=ifelse(str_detect(clean_url,"brentozar"),"B","nB")) %>%
count( email_user_id, brent, topic) %>%
unite(topic_brent,topic, brent) %>%
mutate(topic_brent=paste0("topic_",topic_brent)) %>%
group_by(email_user_id) %>%
mutate(prop=nn/sum(nn)) ->
topic_clicks_l
topic_clicks_l %>%
select(-prop) %>%
mutate(topic_brent=paste0(topic_brent,"_clicks")) %>%
topic_clicks_clicks
topic_clicks_l %>%
select(-nn) %>%
mutate(topic_brent=paste0(topic_brent,"_prop")) %>%
spread(topic_brent, prop, fill=0) ->
topic_clicks_prop
topic_clicks_l %>%
mutate(topic_brent=paste0(topic_brent,"_active")) %>%
group_by(topic_brent,email_user_id) %>%
summarise(active=1) %>%
spread(topic_brent, active, fill=0) ->
topic_clicks_active
(topic_clicks_clicks %>%
inner_join(topic_clicks_prop) %>%
inner_join(topic_clicks_active)->
topic_clicks)
rm(list=c("topics_clicks_l",
"topic_clicks_clicks","topic_clicks_prop",
"topic_clicks_active",
"clicks"))
LS0tDQp0aXRsZTogIkdldHRpbmcgYWxsIHVwIGluIEJyZW50J3MgYml6IC0gRmVhdHVyZSBlbmdpbmVlcmluZyINCm91dHB1dDogDQogIGh0bWxfbm90ZWJvb2s6IA0KICAgIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIHRvYzogeWVzDQogICAgdG9jX2RlcHRoOiAxDQotLS0NCg0KYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQod2FybmluZyA9IEZBTFNFLCBtZXNzYWdlID0gRkFMU0UpDQpsaWJyYXJ5KERCSSkNCmNvbiA8LSBkYkNvbm5lY3Qob2RiYzo6b2RiYygpLCAuY29ubmVjdGlvbl9zdHJpbmcgPSAiRHJpdmVyPXtPREJDIERyaXZlciAxMyBmb3IgU1FMIFNlcnZlcn07c2VydmVyPXticmVudG8uZGF0YWJhc2Uud2luZG93cy5uZXR9Ow0KZGF0YWJhc2U9e2JyZW50b2RifTsNCnVpZD17ZGF0YXNjaX07DQpwd2Q9e25aWTAqNTFsR159OyIpDQoNCiMgY3JlZCBjcmVhdGlvbg0KDQojaW4gbWFzdGVyDQojQ1JFQVRFIExPR0lOIGRhdGFzY2kgV0lUSCBwYXNzd29yZD0nblpZMCo1MWxHXic7DQojIGluIGRiDQojQ1JFQVRFIFVTRVIgZGF0YXNjaSBGUk9NIExPR0lOIGRhdGFzY2k7DQojRVhFQyBzcF9hZGRyb2xlbWVtYmVyICdkYl9kYXRhcmVhZGVyJywgJ2RhdGFzY2knOw0KI2FsdGVyIHVzZXIgZGF0YXNjaSB3aXRoIGRlZmF1bHRfc2NoZW1hPWFub24NCmBgYA0KDQpCYXNlZCBvbiBbdGhlIGV4cGxvcmF0b3J5IGRhdGEgYW5hbHlzaXNdKGJyZW50c2JpekVEQS5odG1sKSwgd2UgY2FuIG5vdyBzdGFydCBwdXR0aW5nIHRvZ2V0aGVyIGRhdGFzZXRzIGFpbWVkIGF0IGFuc3dlcmluZyB0aGUgZm9sbG93aW5nIHF1ZXN0aW9uOg0KDQo+IFdoYXQgaW1wYWN0ZWQgcGVvcGxlJ3MgbGlrZWxpaG9vZCB0byBidXkgdHJhaW5pbmcgaW4gMjAxNz8NCg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgcmVzdWx0cz0naGlkZSd9DQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkoZGJwbHlyKQ0KYGBgDQoNCiMgQnV5aW5nIHRyYWluaW5nDQoNCldobyBib3VnaHQgdHJhaW5pbmcgaW4gMjAxNz8gV2UgbmVlZCB0byBncmFiIHRoZSBkYXRhIGFib3V0IHdobyBib3VnaHQgYW5kIGFsc28gaW5jbHVkZSB0aGUgc3R1ZGVudHMgZW1haWwgYWRkcmVzc2VzIHRvIGFjY291bnQgZm9yIHN0dWRlbnRzIGJlaW5nIHRoZSByZXF1ZXN0ZXJzIGJ1dCBub3QgdGhlIHB1cmNoYXNlcnMuIFdlIHdpbGwgbmVlZCB0byBleGNsdWRlIHRoZSBmcmVlIHRyYWluaW5nIG9wdGlvbnMuDQoNCmBgYHtyfQ0KY29uICU+JSANCiAgdGJsKCJ3b29jb21tZXJjZV9vcmRlcl9pdGVtcyIpIC0+DQogIGl0ZW1zDQoNCmNvbiAlPiUgDQogIHRibCgicG9zdHMiKSAtPg0KICBwb3N0cw0KDQpwb3N0cyAlPiUgDQogIGlubmVyX2pvaW4oaXRlbXMsIGJ5PWMoImlkIj0ib3JkZXJfaWQiKSkgJT4lIA0KICBmaWx0ZXIob3JkZXJfaXRlbV9uYW1lIT0nRnJlZSBNZW1iZXJzaGlwJykgJT4lIA0KICBzZWxlY3Qob3JkZXJfaWQ9aWQsIHB1cmNoYXNlX2RhdGU9cG9zdF9kYXRlX2dtdCwgb3JkZXJfaXRlbV9uYW1lLCBvcmRlcl9pdGVtX2lkKSAlPiUgDQogIGNvbGxlY3QoKSAtPg0KICBvcmRlcnMNCg0KbGlicmFyeShyZWFkeGwpDQoiLi4vZGF0YS9PcmRlciBFbWFpbHMueGxzeCIgJT4lIA0KICByZWFkX2V4Y2VsKCkgJT4lIA0KICBzZWxlY3QoIG9yZGVyX2lkPXBvc3RfaWQsIHN0YXJ0c193aXRoKCJlbWFpbCIpKS0+DQogIG9yZGVyc19wdXJjaGFzZXJzDQoNCmNvbiAlPiUgDQogIHRibCgid29vY29tbWVyY2Vfb3JkZXJfaXRlbXNfbWV0YSIpICU+JSANCiAgbGVmdF9qb2luKGl0ZW1zKSAlPiUgDQogIHNlbGVjdCggb3JkZXJfaWQsIHN0YXJ0c193aXRoKCJlbWFpbCIpKSAlPiUgDQogIGNvbGxlY3QoKSAtPg0KICBvcmRlcnNfc3R1ZGVudHMNCg0Kb3JkZXJzX3B1cmNoYXNlcnMgJT4lIA0KICB1bmlvbihvcmRlcnNfc3R1ZGVudHMpICU+JSANCiAgaW5uZXJfam9pbihvcmRlcnMpLT4NCiAgb3JkZXJzX2VtYWlscw0KDQpvcmRlcnNfZW1haWxzICU+JSANCiAgZ3JvdXBfYnkoZW1haWxfdXNlcl9pZCwgZW1haWxfZG9tYWluX2lkKSAlPiUgDQogIHN1bW1hcmlzZShvcmRlcnM9bigpLA0KICAgICAgICAgICAgZWFybGllc3Q9bWluKHB1cmNoYXNlX2RhdGUpLA0KICAgICAgICAgICAgbGF0ZXN0PW1heChwdXJjaGFzZV9kYXRlKSwNCiAgICAgICAgICAgIHByb2R1Y3RzPWxpc3Qob3JkZXJfaXRlbV9uYW1lKSkgLT4NCiAgY3VzdG9tZXJzDQpgYGANCg0KTGV0J3MgdGFrZSBhIHF1aWNrIGxvb2sgYXQgdGhlIGRpc3RyaWJ1dGlvbiBvZiBvcmRlcnMgYnkgZW1haWwgYWRkcmVzcy4NCg0KYGBge3J9DQpjdXN0b21lcnMgJT4lIA0KICBhcnJhbmdlKGRlc2Mob3JkZXJzKSkNCmBgYA0KDQpgYGB7cn0NCmN1c3RvbWVycyAlPiUgIA0KICBnZ3Bsb3QoYWVzKHg9b3JkZXJzKSkgKw0KICBnZW9tX2hpc3RvZ3JhbShiaW5zPTIwKQ0KYGBgDQoNCldpdGggdGhpcyBsaXN0IG9mIHVuaXF1ZSBlbWFpbCBhZGRyZXNzZXMgaW52b2x2ZWQgaW4gc2FsZXMgd2UgY2FuIHNlZSBob3cgdGhlc2UgbWF0Y2ggdXAgYWdhaW5zdCB0aGUgbWVtYmVyc2hpcCBsaXN0LiBMZXQncyBmaXJzdCBsb29rIGF0IHRoZSBmdWxsIGxpc3QgdG8gc2VlIGhvdyBtYW55IHBlb3BsZSB3ZXJlIG9uIHRoZSBsaXN0IGFuZCB3aGVuIHRoZXkgam9pbmVkLg0KDQoNCmBgYHtyfQ0KY29uICU+JSANCiAgdGJsKCJtYWlsY2hpbXBfbWVtYmVycyIpIC0+DQogIG1haWxjaGltcF9tZW1iZXJzDQoNCmN1c3RvbWVycyAlPiUgDQogIHNlbGVjdCgtcHJvZHVjdHMpICU+JSANCiAgZnVsbF9qb2luKCBtYWlsY2hpbXBfbWVtYmVycywgY29weT1UUlVFLCANCiAgICAgICAgICAgICBieT0gYygiZW1haWxfdXNlcl9pZCIsImVtYWlsX2RvbWFpbl9pZCIpKSAtPg0KICBtYWlsY2hpbXBfY3VzdG9tZXJzDQoNCihtYWlsY2hpbXBfY3VzdG9tZXJzICU+JSANCiAgbXV0YXRlKHB1cmNoYXNlcz0haXMubmEob3JkZXJzKSwNCiAgICAgICAgIG1haWxjaGltcD0haXMubmEobWVtYmVyX3JhdGluZyksDQogICAgICAgICBzaWduZWRfYWZ0ZXJfb3JkZXI9Y29uZmlybV90aW1lPmVhcmxpZXN0KSAlPiUgDQogIGdyb3VwX2J5KG1haWxjaGltcCxwdXJjaGFzZXMsc2lnbmVkX2FmdGVyX29yZGVyKSAlPiUgDQogIHN1bW1hcmlzZShuPW4oKSkgLT4NCiAgbWFpbGNoaW1wX3RyYWluaW5nX292ZXJsYXApDQpgYGANCg0KYGBge3IgZWNobz1GQUxTRX0NCm1haWxjaGltcF90cmFpbmluZ19vdmVybGFwICU+JSANCiAgZmlsdGVyKG1haWxjaGltcCxwdXJjaGFzZXMsICFzaWduZWRfYWZ0ZXJfb3JkZXIpICU+JSANCiAgcHVsbChuKSAlPiUgDQogIHN1bSgpIC0+IA0KICBtYWlsY2hpbXBfYW5kX3B1cmNoYXNlZA0KDQptYWlsY2hpbXBfdHJhaW5pbmdfb3ZlcmxhcCAlPiUgDQogIGZpbHRlcighbWFpbGNoaW1wLHB1cmNoYXNlcykgJT4lIA0KICBwdWxsKG4pICU+JSANCiAgc3VtKCkgLT4gDQogIG5vbWFpbGNoaW1wX2FuZF9wdXJjaGFzZWQNCg0KDQptYWlsY2hpbXBfdHJhaW5pbmdfb3ZlcmxhcCAlPiUgDQogIGZpbHRlcihzaWduZWRfYWZ0ZXJfb3JkZXIpICU+JSANCiAgcHVsbChuKSAlPiUgDQogIHN1bSgpIC0+IA0KICBtYWlsY2hpbXBfYW5kX3B1cmNoYXNlZF9lYXJsaWVyDQpgYGANCg0KTG9va2luZyBhdCB0aGUgcmVjb3JkcywgYHIgc2NhbGVzOjpwZXJjZW50KG5vbWFpbGNoaW1wX2FuZF9wdXJjaGFzZWQvbnJvdyhjdXN0b21lcnMpKWAgb2YgY3VzdG9tZXJzIGRvIG5vdCBoYXZlIGEgbWFpbGNoaW1wIHN1YnNjcmlwdGlvbiAoY3VycmVudGx5KS4gYHIgc2NhbGVzOjpwZXJjZW50KG1haWxjaGltcF9hbmRfcHVyY2hhc2VkX2VhcmxpZXIvbnJvdyhjdXN0b21lcnMpKWAgYXBwZWFyIHRvIGhhdmUgYmVjb21lIG1haWxjaGltcCBtZW1iZXJzIGFmdGVyIHB1cmNoYXNpbmcuIFRoaXMgbGVhdmVzIGByIHNjYWxlczo6cGVyY2VudChtYWlsY2hpbXBfYW5kX3B1cmNoYXNlZC9ucm93KGN1c3RvbWVycykpYCBvZiBjdXN0b21lcnMgYmVpbmcgb24gdGhlIG1haWxpbmcgbGlzdCAqYmVmb3JlKiBtYWtpbmcgdGhlaXIgZmlyc3QgcHVyY2hhc2UuIA0KDQpgYGB7cn0NCm1haWxjaGltcF9jdXN0b21lcnMgJT4lIA0KICBmaWx0ZXIoIWlzLm5hKG1lbWJlcl9yYXRpbmcpKSAlPiUgDQogIG11dGF0ZShhbnlfcHVyY2hhc2U9IWlzLm5hKG9yZGVycyksDQogICAgICAgICBwdXJjaGFzZT0haXMubmEob3JkZXJzKSZjb25maXJtX3RpbWU+ZWFybGllc3QpICAlPiUgDQogIG11dGF0ZSh3YW50X3RvX2dldHM9c3RyX3NwbGl0KGlfd2FudF90b19nZXQsIiwgIikpICU+JSANCiAgdW5uZXN0KHdhbnRfdG9fZ2V0cykgJT4lIA0KICBtdXRhdGUod2FudF90b19nZXRzPWNvYWxlc2NlKHdhbnRfdG9fZ2V0cywiVW5rbm93biByZWNkIiksIG49VFJVRSkgJT4lIA0KICBzcHJlYWQod2FudF90b19nZXRzLCBuLGZpbGwgPSBGQUxTRSkgJT4lIA0KICBtdXRhdGUoc2lnbnVwX3NvdXJjZXM9c3RyX3NwbGl0KHNpZ251cF9zb3VyY2UsIiwgIikpICU+JSANCiAgdW5uZXN0KHNpZ251cF9zb3VyY2VzKSAlPiUgDQogIG11dGF0ZShzaWdudXBfc291cmNlcz1jb2FsZXNjZShzaWdudXBfc291cmNlcywiVW5rbm93biBzb3VyY2UiKSwgbj1UUlVFKSAlPiUgDQogIHNwcmVhZChzaWdudXBfc291cmNlcywgbixmaWxsID0gRkFMU0UpIC0+DQogIG1haWxjaGltcF9sYWJlbGxlZA0KYGBgDQoNCiMjIEEgc3VwZXIgYmFzaWMgbW9kZWwNCldpdGhvdXQgdGFraW5nIGludG8gYmVoYXZpb3VyIGluIHRoZSBydW4gdXAgb2YgcHVyY2hhc2luZywgbGV0J3MgZG8gYSB2ZXJ5IHF1aWNrIGFuZCBkaXJ0eSBtb2RlbCB0byBzZWUgd2hldGhlciBzb21lIG9mIHRoZSBtYWlsY2hpbXAgbWVtYmVyIGxldmVsIGZlYXR1cmVzIGFyZSB1c2VmdWwgYXMtaXMgZm9yIHByZWRpY3RpbmcgcHVyY2hhc2VzLiBXZSdsbCB1c2UgYSBkZWNpc2lvbiB0cmVlIGJhc2VkIG1vZGVsIGZvciB0aGlzLg0KDQpgYGB7ciBmaWcuaGVpZ2h0PTE1fQ0KbGlicmFyeShGRlRyZWVzKQ0KDQptYWlsY2hpbXBfbGFiZWxsZWQgJT4lIA0KICB1bmdyb3VwKCkgJT4lIA0KICBzZWxlY3QoLShlbWFpbF91c2VyX2lkOmNsaWVudF90eXBlKSwtKG9wdGluX3RpbWU6bG9uZ2l0dWRlKSwNCiAgICAgICAgIC10aW1lX3pvbmUsLShyZWdpb246bm90ZXMpLCAtcHVyY2hhc2UpICU+JSANCiAgRkZUcmVlcyhhbnlfcHVyY2hhc2UgfiAuLCBkYXRhPS4sDQogICAgICAgICAgICAgICAgICAgZG8uY29tcCA9IEZBTFNFLA0KICAgICAgICAgICAgICAgICAgIHRyYWluLnAgPSAwLjcsIA0KICAgICAgICAgICAgICAgICAgIHNlbnMudyA9IDAuNzUsDQogICAgICAgICAgICAgICAgICAgZGVjaXNpb24ubGFiZWxzID0gYygiRGlkbid0IHB1cmNoYXNlIiwiUHVyY2hhc2VkIiksDQogICAgICAgICAgcHJvZ3Jlc3MgPSBGQUxTRSkgLT4NCiAgaW5pdGlhbF90cmVlDQoNCnBsb3QoaW5pdGlhbF90cmVlKQ0KYGBgDQoNClRoaXMgaXMgYSBzaW1wbGUgbW9kZWwgdGhhdCBjb3JyZWN0bHkgcHJlZGljdHMgYSBnb29kIHByb3BvcnRpb24gb2YgdGhlIGN1c3RvbWVycyB0aGF0IHB1cmNoYXNlZC4gIA0KYGBge3J9DQppbml0aWFsX3RyZWUNCmBgYA0KDQpUaGlzIG1vZGVsIGhlYXZpbHkgZW1waGFzaXNlZCBjb3JyZWN0bHkgaWRlbnRpZnlpbmcgdGhlIHBlb3BsZSB3aG8gd291bGQgcHVyY2hhc2UgYXQgdGhlIGV4cGVuc2Ugb2YgaW5jbHVkaW5nIG1vcmUgcGVvcGxlIHdobyB3b3VsZG4ndCBwdXJjaGFzZS4gV2UgZ2V0IGEgZ2V0IGEgaGlnaCBkZWdyZWUgb2Ygc2Vuc2l0aXZpdHkgKGNvcnJlY3RseSBwcmVkaWN0aW5nIHRob3NlIHdobyBwdXJjaGFzZWQpIGZyb20gdGhlIG1haWxjaGltcCBlbmdhZ2VtZW50IGZpZ3VyZSwgcGVvcGxlIHRpY2tpbmcgd2hhdCB0eXBlcyBvZiBlbWFpbHMgdGhleSB3YW50IHRvIHJlY2VpdmUgYW5kIHRoZW0gbm90IHdhbnRpbmcgdGhlIE1vbmRheSBlbWFpbC4NCg0KVGhpcyBtb2RlbCB3aWxsIGJlIG91ciBiYXNlbGluZSB3aGVuIHdlIGdldCBpbnRvIGJ1aWxkaW5nIG1vcmUgbW9kZWxzLg0KDQojIyBPcGVucw0KDQpOb3cgdGhhdCB3ZSBoYXZlIGZvciBlYWNoIHBlcnNvbiBvbiB0aGUgbWFpbGluZyBsaXN0LCB3aGV0aGVyIHRoZXkgbWFkZSBhIHB1cmNoYXNlIG9yIG5vdCwgd2Ugc2hvdWxkIGJ1aWxkIHNvbWUgZmVhdHVyZXMgYXJvdW5kIGhvdyBvZnRlbiBwZW9wbGUgaGF2ZSBvcGVuZWQgdGhlIG5ld3NsZXR0ZXJzIGluIHRoZSBwYXN0Lg0KDQpgYGB7cn0NCmxpYnJhcnkocGFkcikNCmxpYnJhcnkodGltZURhdGUpDQoNCm1haWxjaGltcF9sYWJlbGxlZCAlPiUgDQogIHRoaWNrZW4oIndlZWsiLCJzdGFydF93ZWVrIixieT0iY29uZmlybV90aW1lIikgICU+JSANCiAgbXV0YXRlKGVhcmxpZXN0PWNvYWxlc2NlKGVhcmxpZXN0LCBhcy5QT1NJWGN0KCIyMDE3LTEyLTMxIikpKSAlPiUgIA0KICB0aGlja2VuKCJ3ZWVrIiwic2FsZV93ZWVrIixieT0iZWFybGllc3QiKSAlPiUNCiAgc2VsZWN0KGVtYWlsX3VzZXJfaWQsIHN0YXJ0X3dlZWssIHNhbGVfd2VlaykgLT4NCiAgY2hpbXBfbGl0ZQ0KDQoNCm9wZW5zIDwtIGRiR2V0UXVlcnkoY29uLA0KICAgICJzZWxlY3QgDQogICAgZW1haWxfdXNlcl9pZCwgDQogICAgQ09OVkVSVChkYXRlLCBvLmNsaWNrX3RpbWVzdGFtcCkgYXMgY2xpY2tfdGltZXN0YW1wLA0KICAgIGNvdW50KCopIGFzIG4NCiAgICANCiAgICBmcm9tIGFub24ubWFpbGNoaW1wX29wZW5zIG8NCiAgICANCiAgICBncm91cCBieSBlbWFpbF91c2VyX2lkLCANCiAgICBDT05WRVJUKGRhdGUsIG8uY2xpY2tfdGltZXN0YW1wKSIpDQoNCmFzLkRhdGUoIjIwMTEtMDEtMDEiKSAlPiUgDQogIHNlcS5EYXRlKGFzLkRhdGUoIjIwMTgtMDEtMDEiKSwgMSkgLT4NCiAgZGF0ZXNfc2VxDQoNCmRhdGVzX3NlcSAlPiUgDQogIGZvcm1hdCgpICU+JSANCiAgdGltZURhdGUoKSAtPg0KICBkYXRlc190aW1lRGF0ZQ0KDQpob2xpZGF5PC1pc0hvbGlkYXkoZGF0ZXNfdGltZURhdGUsaG9saWRheU5ZU0UoMjAxMToyMDE4KSkNCndlZWtkYXk8LWlzV2Vla2RheShkYXRlc190aW1lRGF0ZSkNCg0KZGF0ZXNfbG9va3VwPC10aWJibGUoY2xpY2tfdGltZXN0YW1wPWRhdGVzX3NlcSwNCiAgICAgICAgICAgICAgICAgICAgIGhvbGlkYXksDQogICAgICAgICAgICAgICAgICAgICB3ZWVrZGF5LA0KICAgICAgICAgICAgICAgICAgICAgd2Vla2VuZD0hd2Vla2RheSkNCm9wZW5zICU+JSANCiAgbGVmdF9qb2luKGRhdGVzX2xvb2t1cCktPg0KICBvcGVucw0KDQpvcGVucyAlPiUgDQogIHRoaWNrZW4oIndlZWsiLCAiY2xpY2siKSAlPiUNCiAgaW5uZXJfam9pbihjaGltcF9saXRlLCBieT0iZW1haWxfdXNlcl9pZCIpICU+JSANCiAgbXV0YXRlKHN0YXJ0X3dlZWtfZGlmZj1kaWZmdGltZShjbGljaywgc3RhcnRfd2VlaywgdW5pdHM9IndlZWsiKSwNCiAgICAgICAgIHNhbGVfd2Vla19kaWZmPWRpZmZ0aW1lKHNhbGVfd2VlaywgY2xpY2ssIHVuaXRzPSJ3ZWVrIikpICU+JSANCiAgbXV0YXRlKHN0YXJ0X21vbnRoPShhcy5pbnRlZ2VyKHN0YXJ0X3dlZWtfZGlmZikgJS8lIDQpICsxLA0KICAgICAgICAgc2FsZV9tb250aD0oYXMuaW50ZWdlcihzYWxlX3dlZWtfZGlmZikgJS8lIDQpICsxICkgIC0+DQogIG9wZW5zDQoNCm9wZW5zICU+JSANCiAgc2FtcGxlX24oMjAwKQ0KYGBgDQoNCiMjIyBPcGVucyBieSB1c2FnZQ0KYGBge3J9DQoob3BlbnMgJT4lIA0KICBncm91cF9ieShlbWFpbF91c2VyX2lkKSAlPiUgDQogIHN1bW1hcmlzZShob2xpZGF5X29wZW5fcmF3PXN1bShob2xpZGF5Km4pLA0KICAgICAgICAgICAgaG9saWRheV9vcGVuX3Byb3A9aG9saWRheV9vcGVuX3Jhdy9zdW0obiksDQogICAgICAgICAgICB3ZWVrZGF5X29wZW5fcmF3PXN1bSh3ZWVrZGF5Km4pLA0KICAgICAgICAgICAgd2Vla2RheV9vcGVuX3Byb3A9d2Vla2RheV9vcGVuX3Jhdy9zdW0obiksDQogICAgICAgICAgICB3ZWVrZW5kX29wZW5fcmF3PXN1bSh3ZWVrZW5kKm4pLA0KICAgICAgICAgICAgd2Vla2VuZF9vcGVuX3Byb3A9d2Vla2VuZF9vcGVuX3Jhdy9zdW0obikNCiAgICAgICAgICAgICkgIC0+DQogIG9wZW5zX3R5cGVvZmRheSkNCmBgYA0KDQojIyMgT3BlbnMgb3ZlciB0aW1lDQoNCmBgYHtyfQ0Kb3BlbnMgJT4lIA0KICBmaWx0ZXIoc3RhcnRfbW9udGggID4gMCkgJT4lIA0KICBtdXRhdGUoc2luY2Vfc3RhcnRfaD1zdGFydF9tb250aCAlLyUgNikgJT4lIA0KICBncm91cF9ieShlbWFpbF91c2VyX2lkLHNpbmNlX3N0YXJ0X2gpICU+JSANCiAgc3VtbWFyaXNlKG9wZW5zPXN1bShuKSxhY3RpdmU9VFJVRSkgJT4lIA0KICBncm91cF9ieShlbWFpbF91c2VyX2lkKSAlPiUgDQogIG11dGF0ZShvcGVuX3Byb3A9b3BlbnMvc3VtKG9wZW5zKSwNCiAgICAgICAgIHNpbmNlX3N0YXJ0X2g9cGFzdGUwKCJoIixzaW5jZV9zdGFydF9oKSkgLT4NCiAgb3BlbnNfc3RhcnRfaGwNCg0Kb3BlbnNfc3RhcnRfaGwgJT4lIA0KICBzZWxlY3QoZW1haWxfdXNlcl9pZDpzaW5jZV9zdGFydF9oLCBvcGVucykgJT4lIA0KICBtdXRhdGUoc2luY2Vfc3RhcnRfaD1wYXN0ZTAoc2luY2Vfc3RhcnRfaCwiX29wZW5zIikpICU+JSANCiAgc3ByZWFkKHNpbmNlX3N0YXJ0X2gsIG9wZW5zLCBmaWxsPTApIC0+DQogIG9wZW5zX3N0YXJ0X29wZW5zDQoNCm9wZW5zX3N0YXJ0X2hsICU+JSANCiAgc2VsZWN0KGVtYWlsX3VzZXJfaWQ6c2luY2Vfc3RhcnRfaCwgYWN0aXZlKSAlPiUgDQogIG11dGF0ZShzaW5jZV9zdGFydF9oPXBhc3RlMChzaW5jZV9zdGFydF9oLCJfb3BlbnNhY3RpdmUiKSkgJT4lIA0KICBzcHJlYWQoc2luY2Vfc3RhcnRfaCwgYWN0aXZlLCBmaWxsPUZBTFNFKSAtPg0KICBvcGVuc19zdGFydF9hY3RpdmUNCg0Kb3BlbnNfc3RhcnRfaGwgJT4lIA0KICBzZWxlY3QoZW1haWxfdXNlcl9pZDpzaW5jZV9zdGFydF9oLCBvcGVuX3Byb3ApICU+JSANCiAgbXV0YXRlKHNpbmNlX3N0YXJ0X2g9cGFzdGUwKHNpbmNlX3N0YXJ0X2gsIl9vcGVuc3Byb3AiKSkgJT4lIA0KICBzcHJlYWQoc2luY2Vfc3RhcnRfaCwgb3Blbl9wcm9wLCBmaWxsPTApIC0+DQogIG9wZW5zX3N0YXJ0X3Byb3ANCg0KKG9wZW5zX3N0YXJ0X29wZW5zICU+JSANCiAgbGVmdF9qb2luKG9wZW5zX3N0YXJ0X2FjdGl2ZSkgJT4lIA0KICBsZWZ0X2pvaW4ob3BlbnNfc3RhcnRfcHJvcCkgLT4NCiAgb3BlbnNfYWN0aXZpdHkpDQoNCmBgYA0KDQoNCg0KYGBge3IgY29sbGFwc2U9VFJVRX0NCnJtKGxpc3Q9Yygib3BlbnMiLCJvcGVuc19zdGFydF9obCIsDQogICAgICAgICAgIm9wZW5zX3N0YXJ0X29wZW5zIiwib3BlbnNfc3RhcnRfYWN0aXZlIiwNCiAgICAgICAgICAib3BlbnNfc3RhcnRfcHJvcCIpKQ0KDQpgYGANCg0KIyMgQ2xpY2tzDQpgYGB7cn0NCmNsaWNrcyA8LSBkYkdldFF1ZXJ5KGNvbiwNCiAgICAic2VsZWN0IA0KICAgIGVtYWlsX3VzZXJfaWQsIA0KICAgIENPTlZFUlQoZGF0ZSwgby5jbGlja190aW1lc3RhbXApIGFzIGNsaWNrX3RpbWVzdGFtcCwNCiAgICBvLmNsaWNrX3VybCwNCiAgICBjb3VudCgqKSBhcyBuDQogICAgDQogICAgZnJvbSBhbm9uLm1haWxjaGltcF9jbGlja3Mgbw0KICAgIA0KICAgIGdyb3VwIGJ5IGVtYWlsX3VzZXJfaWQsIA0KICAgIENPTlZFUlQoZGF0ZSwgby5jbGlja190aW1lc3RhbXApLA0KICAgIG8uY2xpY2tfdXJsIikNCg0KDQpjbGlja3MgJT4lIA0KICBsZWZ0X2pvaW4oZGF0ZXNfbG9va3VwKS0+DQogIGNsaWNrcw0KDQpjbGlja3MgJT4lIA0KICB0aGlja2VuKCJ3ZWVrIiwgImNsaWNrIikgJT4lDQogIGlubmVyX2pvaW4oY2hpbXBfbGl0ZSwgYnk9ImVtYWlsX3VzZXJfaWQiKSAlPiUgDQogIG11dGF0ZShzdGFydF93ZWVrX2RpZmY9ZGlmZnRpbWUoY2xpY2ssIHN0YXJ0X3dlZWssIHVuaXRzPSJ3ZWVrIiksDQogICAgICAgICBzYWxlX3dlZWtfZGlmZj1kaWZmdGltZShzYWxlX3dlZWssIGNsaWNrLCB1bml0cz0id2VlayIpKSAlPiUgDQogIG11dGF0ZShzdGFydF9tb250aD0oYXMuaW50ZWdlcihzdGFydF93ZWVrX2RpZmYpICUvJSA0KSArMSwNCiAgICAgICAgIHNhbGVfbW9udGg9KGFzLmludGVnZXIoc2FsZV93ZWVrX2RpZmYpICUvJSA0KSArMSApICAtPg0KICBjbGlja3MNCg0KYGBgDQoNCg0KIyMjIENsaWNrcyBieSB1c2FnZQ0KYGBge3J9DQooY2xpY2tzICU+JSANCiAgZ3JvdXBfYnkoZW1haWxfdXNlcl9pZCkgJT4lIA0KICBzdW1tYXJpc2UoaG9saWRheV9jbGlja3NfcmF3PXN1bShob2xpZGF5Km4pLA0KICAgICAgICAgICAgaG9saWRheV9jbGlja3NfcHJvcD1ob2xpZGF5X2NsaWNrc19yYXcvc3VtKG4pLA0KICAgICAgICAgICAgd2Vla2RheV9jbGlja3NfcmF3PXN1bSh3ZWVrZGF5Km4pLA0KICAgICAgICAgICAgd2Vla2RheV9jbGlja3NfcHJvcD13ZWVrZGF5X2NsaWNrc19yYXcvc3VtKG4pLA0KICAgICAgICAgICAgd2Vla2VuZF9jbGlja3NfcmF3PXN1bSh3ZWVrZW5kKm4pLA0KICAgICAgICAgICAgd2Vla2VuZF9jbGlja3NfcHJvcD13ZWVrZW5kX2NsaWNrc19yYXcvc3VtKG4pDQogICAgICAgICAgICApICAtPg0KICBjbGlja3NfdHlwZW9mZGF5KQ0KYGBgDQoNCiMjIyBDbGlja3Mgb3ZlciB0aW1lDQoNCmBgYHtyfQ0KY2xpY2tzICU+JSANCiAgZmlsdGVyKHN0YXJ0X21vbnRoICA+IDApICU+JSANCiAgbXV0YXRlKHNpbmNlX3N0YXJ0X2g9c3RhcnRfbW9udGggJS8lIDYpICU+JSANCiAgZ3JvdXBfYnkoZW1haWxfdXNlcl9pZCxzaW5jZV9zdGFydF9oKSAlPiUgDQogIHN1bW1hcmlzZShjbGlja3M9c3VtKG4pLGFjdGl2ZT1UUlVFKSAlPiUgDQogIGdyb3VwX2J5KGVtYWlsX3VzZXJfaWQpICU+JSANCiAgbXV0YXRlKGNsaWNrX3Byb3A9Y2xpY2tzL3N1bShjbGlja3MpLA0KICAgICAgICAgc2luY2Vfc3RhcnRfaD1wYXN0ZTAoImgiLHNpbmNlX3N0YXJ0X2gpKSAtPg0KICBjbGlja3Nfc3RhcnRfaGwNCg0KY2xpY2tzX3N0YXJ0X2hsICU+JSANCiAgc2VsZWN0KGVtYWlsX3VzZXJfaWQ6c2luY2Vfc3RhcnRfaCwgY2xpY2tzKSAlPiUgDQogIG11dGF0ZShzaW5jZV9zdGFydF9oPXBhc3RlMChzaW5jZV9zdGFydF9oLCJfY2xpY2tzIikpICU+JSANCiAgc3ByZWFkKHNpbmNlX3N0YXJ0X2gsIGNsaWNrcywgZmlsbD0wKSAtPg0KICBjbGlja3Nfc3RhcnRfY2xpY2tzDQoNCmNsaWNrc19zdGFydF9obCAlPiUgDQogIHNlbGVjdChlbWFpbF91c2VyX2lkOnNpbmNlX3N0YXJ0X2gsIGFjdGl2ZSkgJT4lIA0KICBtdXRhdGUoc2luY2Vfc3RhcnRfaD1wYXN0ZTAoc2luY2Vfc3RhcnRfaCwiX2NsaWNrYWN0aXZlIikpICU+JSANCiAgc3ByZWFkKHNpbmNlX3N0YXJ0X2gsIGFjdGl2ZSwgZmlsbD1GQUxTRSkgLT4NCiAgY2xpY2tzX3N0YXJ0X2FjdGl2ZQ0KDQpjbGlja3Nfc3RhcnRfaGwgJT4lIA0KICBzZWxlY3QoZW1haWxfdXNlcl9pZDpzaW5jZV9zdGFydF9oLCBjbGlja19wcm9wKSAlPiUgDQogIG11dGF0ZShzaW5jZV9zdGFydF9oPXBhc3RlMChzaW5jZV9zdGFydF9oLCJfY2xpY2twcm9wIikpICU+JSANCiAgc3ByZWFkKHNpbmNlX3N0YXJ0X2gsIGNsaWNrX3Byb3AsIGZpbGw9MCkgLT4NCiAgY2xpY2tzX3N0YXJ0X3Byb3ANCg0KKGNsaWNrc19zdGFydF9jbGlja3MgJT4lIA0KICBsZWZ0X2pvaW4oY2xpY2tzX3N0YXJ0X2FjdGl2ZSkgJT4lIA0KICBsZWZ0X2pvaW4oY2xpY2tzX3N0YXJ0X3Byb3ApIC0+DQogIGNsaWNrX2FjdGl2aXR5KQ0KDQpgYGANCg0KDQoNCmBgYHtyIGNvbGxhcHNlPVRSVUV9DQpybShsaXN0PWMoImNsaWNrc19zdGFydF9obCIsDQogICAgICAgICAgImNsaWNrc19zdGFydF9jbGlja3MiLCJjbGlja3Nfc3RhcnRfYWN0aXZlIiwNCiAgICAgICAgICAiY2xpY2tzX3N0YXJ0X3Byb3AiKSkNCg0KYGBgDQoNCiMjIyBDbGlja3MgYnkgdG9waWMNCg0KRm9yIGhvdyB3ZSBhcnJpdmVkIGF0IHRvcGljcyBwZXIgdXJsLCBjaGVjayBvdXQgW1doYXQgdG9waWNzIGRpZCBwZW9wbGUgY2xpY2s/XShicmVudHNiaXppbnRlcm5ldC5uYi5odG1sKQ0KDQpgYGB7cn0NCmNsaWNrcyAlPiUgDQogIGlubmVyX2pvaW4oew0KICAgICIuLi9kYXRhL2xpbmtfbG9va3VwLmNzdiIgJT4lIA0KICAgIHJlYWRfY3N2KCkNCiAgfSwgYnk9ImNsaWNrX3VybCIpICU+JSANCiAgbGVmdF9qb2luKHsNCiAgICAiLi4vZGF0YS91cmx0b3BpY3MuY3N2IiU+JSANCiAgICByZWFkX2NzdigpDQogIH0sIGJ5PWMoImNsZWFuX2ZpbGUiPSJ1cmwiKSkgJT4lIA0KICBtdXRhdGUoYnJlbnQ9aWZlbHNlKHN0cl9kZXRlY3QoY2xlYW5fdXJsLCJicmVudG96YXIiKSwiQiIsIm5CIikpICU+JSANCiAgY291bnQoIGVtYWlsX3VzZXJfaWQsIGJyZW50LCB0b3BpYykgJT4lIA0KICB1bml0ZSh0b3BpY19icmVudCx0b3BpYywgYnJlbnQpICU+JSANCiAgbXV0YXRlKHRvcGljX2JyZW50PXBhc3RlMCgidG9waWNfIix0b3BpY19icmVudCkpICU+JSANCiAgZ3JvdXBfYnkoZW1haWxfdXNlcl9pZCkgJT4lIA0KICBtdXRhdGUocHJvcD1ubi9zdW0obm4pKSAtPg0KICB0b3BpY19jbGlja3NfbA0KDQp0b3BpY19jbGlja3NfbCAlPiUgDQogIHNlbGVjdCgtcHJvcCkgJT4lIA0KICBtdXRhdGUodG9waWNfYnJlbnQ9cGFzdGUwKHRvcGljX2JyZW50LCJfY2xpY2tzIikpICU+JSANCiAgc3ByZWFkKHRvcGljX2JyZW50LCBubiwgZmlsbD0wKSAtPg0KICB0b3BpY19jbGlja3NfY2xpY2tzDQoNCnRvcGljX2NsaWNrc19sICU+JSANCiAgc2VsZWN0KC1ubikgJT4lIA0KICBtdXRhdGUodG9waWNfYnJlbnQ9cGFzdGUwKHRvcGljX2JyZW50LCJfcHJvcCIpKSAlPiUgDQogIHNwcmVhZCh0b3BpY19icmVudCwgcHJvcCwgZmlsbD0wKSAtPg0KICB0b3BpY19jbGlja3NfcHJvcA0KDQp0b3BpY19jbGlja3NfbCAlPiUgDQogIG11dGF0ZSh0b3BpY19icmVudD1wYXN0ZTAodG9waWNfYnJlbnQsIl9hY3RpdmUiKSkgJT4lIA0KICBncm91cF9ieSh0b3BpY19icmVudCxlbWFpbF91c2VyX2lkKSAlPiUgDQogIHN1bW1hcmlzZShhY3RpdmU9MSkgJT4lIA0KICBzcHJlYWQodG9waWNfYnJlbnQsIGFjdGl2ZSwgZmlsbD0wKSAtPg0KICB0b3BpY19jbGlja3NfYWN0aXZlDQoNCih0b3BpY19jbGlja3NfY2xpY2tzICU+JSANCiAgaW5uZXJfam9pbih0b3BpY19jbGlja3NfcHJvcCkgJT4lIA0KICBpbm5lcl9qb2luKHRvcGljX2NsaWNrc19hY3RpdmUpLT4NCiAgdG9waWNfY2xpY2tzKQ0KYGBgDQoNCg0KYGBge3IgY29sbGFwc2U9VFJVRX0NCnJtKGxpc3Q9YygidG9waWNzX2NsaWNrc19sIiwNCiAgICAgICAgICAidG9waWNfY2xpY2tzX2NsaWNrcyIsInRvcGljX2NsaWNrc19wcm9wIiwNCiAgICAgICAgICAidG9waWNfY2xpY2tzX2FjdGl2ZSIsDQogICAgICAgICAgImNsaWNrcyIpKQ0KDQpgYGANCg0KDQojIyBQdXR0aW5nIGl0IGFsbCB0b2dldGhlcg0KDQpgYGB7cn0NCm1haWxjaGltcF9sYWJlbGxlZCAlPiUgDQogIGxlZnRfam9pbihvcGVuc190eXBlb2ZkYXksIGJ5PSJlbWFpbF91c2VyX2lkIikgJT4lIA0KICBsZWZ0X2pvaW4ob3BlbnNfYWN0aXZpdHksIGJ5PSJlbWFpbF91c2VyX2lkIikgJT4lIA0KICBsZWZ0X2pvaW4oY2xpY2tzX3R5cGVvZmRheSwgYnk9ImVtYWlsX3VzZXJfaWQiKSAlPiUgDQogIGxlZnRfam9pbihjbGlja19hY3Rpdml0eSwgYnk9ImVtYWlsX3VzZXJfaWQiKSAlPiUgDQogIGxlZnRfam9pbih0b3BpY19jbGlja3MsIGJ5PSJlbWFpbF91c2VyX2lkIikgLT4gDQogIG1haWxjaGltcF93aWRlDQoNCm1haWxjaGltcF93aWRlICU+JSANCiAgc2VsZWN0KC0oZW1haWxfZG9tYWluX2lkOmNsaWVudF90eXBlKSwtKG9wdGluX3RpbWU6dGltZV96b25lKSwNCiAgICAgICAgIC0ocmVnaW9uOm5vdGVzKSkgJT4lIA0KICBtdXRhdGUobmV2ZXJfb3BlbmVkPWlzLm5hKGhvbGlkYXlfb3Blbl9yYXcpLA0KICAgICAgICAgbmV2ZXJfY2xpY2tlZD1pcy5uYShob2xpZGF5X2NsaWNrc19yYXcpKSAtPiANCiAgbWFpbGNoaW1wX3dpZGUNCg0KbWFpbGNoaW1wX3dpZGU8LW11dGF0ZV9pZihtYWlsY2hpbXBfd2lkZSwgaXMubG9naWNhbCwgfmNvYWxlc2NlKC4sRkFMU0UpKQ0KbWFpbGNoaW1wX3dpZGU8LW11dGF0ZV9pZihtYWlsY2hpbXBfd2lkZSwgaXNfZG91YmxlLCB+Y29hbGVzY2UoLiwwKSkNCm1haWxjaGltcF93aWRlPC1tdXRhdGVfaWYobWFpbGNoaW1wX3dpZGUsIGlzX2ludGVnZXIsIH5jb2FsZXNjZSguLDBMKSkgDQoNCndyaXRlX2NzdihtYWlsY2hpbXBfd2lkZSwiLi4vb3V0cHV0cy9maW5hbGRhdGFzZXQuY3N2IikNCg0KbWFpbGNoaW1wX3dpZGUNCg0KYGBgDQo=